package com.davfx.httpnio;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;

// It would be great to automatically close kept-alive connections when over time
// but it greatly complexify the model (because the InnerResponseWriter.close method can be called out of the worker thread)

final class HttpRequestReader implements ReadHandler {

	private final LineReader lineReader = new LineReader();
	private boolean headersRead = false;
	private boolean requestLineRead = false;
	private long contentLength;
	private int countRead = 0;
	private HttpRequest.Method requestMethod;
	private String requestPath;
	private boolean http11;
	private final Map<String, String> headers = new HashMap<String, String>();
	private boolean failClose = true;
	
	private final class InnerResponseWriter implements ResponseWriter {
		private long countWrite = 0;
		private long writeContentLength = -1;
		private boolean chunked = false;
		private final boolean enableGzip;
		private GzipWriter gzipWriter;
		
		private final WriteHandler innerWriteHandler;
		
		public InnerResponseWriter(boolean enableGzip, final HttpReadListenHandler handler) {
			this.enableGzip = enableGzip;
			innerWriteHandler = new WriteHandler() {
				@Override
				public void failed(IOException e) {
					closeFrom(handler, e);
				}
			};
		}
		
		@Override
		public void close() {
			// note: we cannot test 'canceled' here, because we're not ensured that we're in the worker thread
			
			if (gzipWriter != null) {
				gzipWriter.close();
			}
			
			if (http11) {
				if (chunked) {
					connection.write(LineReader.toBuffer(Integer.toHexString(0)), innerWriteHandler);
					connection.write(LineReader.toBuffer(""), innerWriteHandler);
					return;
				}
				
	//			worker.add(new SelectorVisitor() { // not required to be pushed to worker because unique object
	//				@Override
	//				public void visit(Selector selector) {

				if ((writeContentLength >= 0) && (countWrite == writeContentLength)) {
					return; // keep alive
				}
			}
			
			connection.close();
//				}
//			});
		}

		@Override
		public void write(int statusCode, String reason, Map<String, String> headers) {
			// note: we cannot test 'canceled' here, because we're not ensured that we're in the worker thread

			if (http11) {
				if (enableGzip && Http.GZIP.equalsIgnoreCase(headers.get(Http.CONTENT_ENCODING))) {
					gzipWriter = new GzipWriter(new ByteBufferHandler() {
						@Override
						public void handle(ByteBuffer buffer) {
							doWrite(buffer);
						}
					});
					chunked = true;
					// deflated length != source length
				} else {
					chunked = Http.CHUNKED.equalsIgnoreCase(headers.get(Http.TRANSFER_ENCODING));
				}
			}
			
			String contentLengthValue = headers.get(Http.CONTENT_LENGTH);
			if (contentLengthValue != null) {
				try {
					writeContentLength = Integer.parseInt(contentLengthValue);
				} catch (NumberFormatException e) {
				}
			}
			
			connection.write(LineReader.toBuffer((http11 ? Http.HTTP11 : Http.HTTP10) + Http.START_LINE_SEPARATOR + statusCode + Http.START_LINE_SEPARATOR + reason), innerWriteHandler);
			for (Map.Entry<String, String> h : headers.entrySet()) {
				String k = h.getKey();
				String v = h.getValue();
				if (gzipWriter != null) {
					if (k.equals(Http.CONTENT_LENGTH)) {
						continue;
					}
					if (k.equals(Http.TRANSFER_ENCODING)) {
						continue;
					}
				}
				if (!http11) {
					if (k.equals(Http.CONTENT_ENCODING)) {
						continue;
					}
				}
				connection.write(LineReader.toBuffer(k + Http.HEADER_KEY_VALUE_SEPARATOR + Http.HEADER_BEFORE_VALUE + v), innerWriteHandler);
			}
			if (gzipWriter != null) {
				connection.write(LineReader.toBuffer(Http.TRANSFER_ENCODING + Http.HEADER_KEY_VALUE_SEPARATOR + Http.HEADER_BEFORE_VALUE + Http.CHUNKED), innerWriteHandler);
			}
			connection.write(LineReader.toBuffer(""), innerWriteHandler);
		}
		@Override
		public void write(ByteBuffer buffer) {
			// note: we cannot test 'canceled' here, because we're not ensured that we're in the worker thread

			if (gzipWriter != null) {
				gzipWriter.handle(buffer);
			} else {
				doWrite(buffer);
			}
		}
		private void doWrite(ByteBuffer buffer) {
			if (!buffer.hasRemaining()) {
				return;
			}
			if (chunked) {
				connection.write(LineReader.toBuffer(Integer.toHexString(buffer.remaining())), innerWriteHandler);
			}
			countWrite += buffer.remaining();
			connection.write(buffer, innerWriteHandler);
			if (chunked) {
				connection.write(LineReader.toBuffer(""), innerWriteHandler);
			}
		}
	}
	
	private final HttpListenFactory factory;
	private final Connection connection;
	private HttpReadListenHandler handler = null;
	private InnerResponseWriter currentResponseWriter;
	
	public HttpRequestReader(HttpListenFactory factory, Connection connection) {
		this.factory = factory;
		this.connection = connection;
	}
	
	private void addHeader(String headerLine) throws IOException {
		int i = headerLine.indexOf(Http.HEADER_KEY_VALUE_SEPARATOR);
		if (i < 0) {
			throw new IOException("Invalid header: " + headerLine);
		}
		String key = headerLine.substring(0, i);
		String value = headerLine.substring(i + 1).trim();
		headers.put(key, value);
	}
	private void setRequestLine(String requestLine) throws IOException {
		int i = requestLine.indexOf(Http.START_LINE_SEPARATOR);
		if (i < 0) {
			throw new IOException("Invalid request: " + requestLine);
		}
		int j = requestLine.indexOf(Http.START_LINE_SEPARATOR, i + 1);
		if (j < 0) {
			throw new IOException("Invalid request: " + requestLine);
		}
		requestMethod = null;
		String m = requestLine.substring(0, i);
		for (HttpRequest.Method method : HttpRequest.Method.values()) {
			if (method.toString().equals(m)) {
				requestMethod = method;
				break;
			}
		}
		if (requestMethod == null) {
			throw new IOException("Invalid request: " + requestLine);
		}
		requestPath = requestLine.substring(i + 1, j);
		String requestVersion = requestLine.substring(j + 1);
		if (requestVersion.equals(Http.HTTP10)) {
			http11 = false;
		} else if (requestVersion.equals(Http.HTTP11)) {
			http11 = true;
		} else {
			throw new IOException("Unsupported version");
		}
	}
	
	private void closeFrom(HttpReadListenHandler from, IOException e) {
		connection.close();
		if (from == handler) {
			handler.failed(e);
			handler = null;
			currentResponseWriter = null;
		}
	}
	
	public void closed() {
		if (failClose) {
			connection.close();
			if (handler != null) {
				handler.failed(new IOException("Connection reset by peer"));
			}
		} else {
			if (handler != null) {
				handler.closed();
			}
		}
		handler = null;
		currentResponseWriter = null;
	}
	
	public void failed(IOException e) {
		connection.close();
		if (handler != null) {
			handler.failed(e);
		}
		handler = null;
		currentResponseWriter = null;
	}
	
	public void handle(ByteBuffer b) {
		try {

			failClose = true;
			
			while (!requestLineRead) {
				String line = lineReader.handle(b);
				if (line == null) {
					return;
				}
				if (handler != null) {
					handler.closed();
					handler = null;
					currentResponseWriter = null;
				}
				setRequestLine(line);
				requestLineRead = true;
			}
	
			while (!headersRead) {
				String line = lineReader.handle(b);
				if (line == null) {
					return;
				}
				if (line.isEmpty()) {
					headersRead = true;
					handler = factory.create();
					String accept = headers.get(Http.ACCEPT_ENCODING);
					boolean enableGzip = false;
					if (accept != null) {
						String[] list = accept.split("\\" + Http.MULTIPLE_SEPARATOR);
						for (String s : list) {
							String[] v = s.trim().split("\\" + Http.EXTENSION_SEPARATOR);
							if (v.length > 0) {
								if (v[0].trim().equalsIgnoreCase(Http.GZIP)) {
									enableGzip = true;
									break;
								}
							}
						}
					}
					currentResponseWriter = new InnerResponseWriter(enableGzip, handler);
					handler.handle(connection.getHost(), connection.getPort(), requestMethod, requestPath, headers);
					String contentLengthValue = headers.get(Http.CONTENT_LENGTH);
					if (contentLengthValue != null) {
						try {
							contentLength = Long.parseLong(contentLengthValue);
						} catch (NumberFormatException e) {
							throw new IOException("Invalid Content-Length: " + contentLengthValue);
						}
					} else {
						contentLength = 0;
					}
				} else {
					addHeader(line);
				}
			}
			
			if (countRead < contentLength) {
				ByteBuffer d = b;
				long toRead = contentLength - countRead;
				if (b.remaining() > toRead) {
					d = b.duplicate();
					d.limit((int) (b.position() + toRead));
					b.position((int) (b.position() + toRead));
				}
				countRead += d.remaining();
				handler.handle(d);
			}

			if (countRead == contentLength) {
				failClose = false;
				countRead = 0;
				requestLineRead = false; // another connection possible
				headersRead = false;
				handler.ended(currentResponseWriter);
			}
			
		} catch (IOException e) {
			connection.close();
			if (handler != null) {
				handler.failed(e);
				handler = null;
			}
		}
	}
	
}
